Récursivité

Table des matières

1. Fonctions récursives

1.1. Fonction factorielle

La factorielle d'un nombre peut être définie par récurrence : $$\begin{cases} 0! = 1 \\ \forall n\in\N^*,\, n! = n \times (n-1)! \end{cases}.$$

En Python, une fonction peut s'appeler elle-même. Une telle fonction est dit récursive. L'extrait de gauche suivant définit une fonction fact qui calcule la factorielle d'un entier en argument.

# Version récursive
def fact(n):
    if n == 0:
        return 1 # 0! = 1
    else:
        return n * fact(n-1) # appel récursif
# Version itérative
def fact(n):
    s = 1
    for i in range(1,n+1):
        s = s * i
    return s

Écrire une fonction récursive qui prend en argument $n\in\N$ et renvoie $S_n = \sum_{k=1}^n k^4$. On a $S_0 = 0$.

Écrire deux fonctions récursives qui prennent en argument un entier $n\geq 1$ et affichent les figures suivantes.

On rappelle que l'instruction print("*" * n) affiche une ligne de $n$ caractères "*".

In [1]: triangle1(5)
*****
****
***
**
*
In [1]: triangle2(5)
*
**
***
****
*****

Écrire une fonction liste qui prend en argument un entier $n$, et renvoie une liste comme celle-ci.

assert(liste(4) == [1 , 2 , 1 , 3 , 1 , 2 , 1 , 4 , 1 , 2 , 1 , 3 , 1 , 2 , 1])

1.2. Des fonctions récursives

Décrire ce que fait chacune des fonctions suivantes.

def mystere1(n):
    assert(n >= 0)
    if n == 0:
        return True
    elif n == 1:
        return False
    else:
        return mystere1(n-2)
def mystere2(a, b):
    assert(a >= 0)
    assert(b >= 0)
    if b == 0:
        return a
    else:
        return mystere2(a+1, b-1)


def mystere3(x, n):
    assert(n >= 0)
    assert(x >= 0)
    if n == 0:
        return 1
    else:
        return x * mystere3(x, n-1)
def mystere4(a):
    assert(a >= 0)
    if a == 0:
        return 0
    r = a % 10
    q = a // 10
    return r + mystere4(q)


1.3. Le problème de l'arrêt

Pour vérifier la terminaison d'une fonction récursive, il faut vérifier que n'importe quel appel finit par se ramener à un des «cas de base», sans quoi la fonction ne s'arrêtera pas.

En pratique, la terminaison est justifiée par le fait que la fonction soit appelée avec des arguments entiers positifs strictement décroissants.

  1. Expliquer pourquoi la fonction f ci-contre n'est pas correcte.
  2. Comment la modifier pour que l'appel f(n) renvoie $\lfloor \frac{n}{2}\rfloor$ pour tout entier naturel $n$ ? et $\lfloor \frac{n+1}{2}\rfloor$ ?
def f(n):
    # Le cas de base, sans appel récursif
    if n == 0:
        return 0
    else:
        # appel récursif
        return 1 + f(n-2)

Les fonctions suivantes s'arrêtent-elles pour n'importe quelle valeur de $\N$ de la variable $n$ ? Justifier brièvement en explicitant un cycle infini d'appels s'il en existe un.

def g1(n):
    if n == 0:
        return 0
    else:
        m = n // 2
        return 1 + g1(m)

def g2(n):
    if n == 0:
        return 1
    if n % 2 == 0:
        return 1+g2(n//2)
    else:
        return g2(n + 1)
def g3(n):
    if n == 0:
        return 1
    if n == 1:
        return 1
    m = n // 2
    return 1 + g3(m + 1)
def g4(n): # βigstarβigstar
    if n == 0:
        return 1
    if n % 2 == 0:
        return g4(n //2 )
    else:
        return g4(3*n+1)


1.4. Pile de récursion et suite de Fibonacci

1.4.1. Pile de récursion

On considère la fonction f suivante.

def f(n):
    if n == 0:
        return 0
    else:
        return 1 + f(n-1)

Pour calculer f(3), la machine virtuelle procède comme décrit à droite.

Pour cela, elle doit garder en mémoire tous les appels récursifs en cours, dans ce qu'on appelle la pile de récursion.

La taille de cette pile est limitée (à 1000 par défaut) ; il y a donc une limite à la profondeur des appels récursifs.

En pratique, l'appel f(10000) échoue avec l'erreur RecursionError: maximum recursion depth exceeded in comparison.

\hspace{1cm}

- elle rentre dans l'appel f(3)
- elle appelle f(2)
   - elle rentre dans l'appel f(2)
   - elle appelle f(1)
      - elle rentre dans l'appel f(1)
      - elle appelle f(0)
         - elle rentre dans l'appel f(0)
         - elle renvoie 0
      - l'appel f(1) renvoie 1 + 0 = 1
   - l'appel f(2) renvoie 1 + 1 = 2
- l'appel f(3) renvoie 1 + 2 = 3

1.4.2. Suite de Fibonacci : explosion des appels récursifs

La suite de Fibonacci $(F_n)_{n\in\N}$ est définie par $F_0 = 1$, $F_1 = 1$ et la relation de récurrence double $\forall n\in\N,\, F_{n+2} = F_{n+1} + F_n$.

  1. Écrire une fonction récursive fib_rec qui calcule $F_n$.
  2. Écrire une version non récursive fib qui calcule $F_n$, avec une complexité linéaire.

On note $T_n$ le nombre total d'appels récursifs que réalise l'appel fib_rec(n). Cette suite vérifie $$T_0 = 0,\,T_1 = 0,\,T_2 = 2 \quad\et\quad \forall n\geq 2,\quad T_{n} = 2 + T_{n-1} + T_{n-2}.$$

Montrer qu'il existe $\a\gt 0$ tel que $T_n \underset{n\ra +\i}{\sim} \a \phi^n$, où $\phi = \frac{1 + \sqrt{5}}{2}$. 1

fib_rec.png

  1. La version récursive de la fonction fib est beaucoup plus simple à écrire, mais elle a une complexité exponentielle.
  2. Ici, le problème n'est pas la profondeur de la pile, qui est de l'ordre de $n$, mais l'explosion du nombre d'appels récursifs.

1.4.3. Mémoïsation ★

La mémoïsation résout le problème de l'explosion des appels récursifs, en gardant en mémoire les valeurs qui ont déjà été calculées, dans une liste, ou un dictionnaire.

l = [1,1] # l[0] = F₀, l[1] = F₁
def mem_fibo_rec(n):
    if len(l)>n:
        return l[n]
    e = mem_fibo_rec(n-2) + mem_fibo_rec(n-1)
    # après l'appel fibo(n-1), la liste l contient
    # toutes les valeurs de la suite jusqu'a Fₙ₋₁
    assert(len(l) == n)
    l.append(e) # l[n] = Fₙ
    return e
# Version avec un dictionnaire
# Plus générale
d = {0: 1, 1: 1}
def mem_fibo_rec(n):
    if n in d:
        return d[n]
    e = mem_fibo_rec(n-2) + mem_fibo_rec(n-1)
    d[n] = e
    return e

La complexité de ces deux variantes est linéaire en $n$, comme la version itérative, mais elles utilisent une liste ou un dictionnaire de taille $n$, donc consomment plus de mémoire. On dit que leur complexité spatiale est linéaire.

2. Algorithmes récursifs classiques

2.1. Exponentiation rapide

Écrire une fonction récursive puiss_naive qui prend en argument un flottant x et un entier naturel n et qui renvoie $x^n$.

On utilisera la propriété $\forall n\in\N,\, x^{n+1} = x \times x^n$.

  1. En utilisant les propriétés

    • $x^{2p} = x^p \times x^p$
    • $x^{2p+1} = x \times x^p \times x^p$

    écrire une variante puissance_rapide de la fonction précédente, pour des exposants $n\geq 0$.

  2. Si $n = 2^m$, combien de produits est-ce que l'appel puissance_rapide(x, n) réalise ? Justifier brièvement.

Dans le cas général, le nombre d'appels récursifs est d'au plus $\log_2(n)$, donc le nombre de produits réalisés est en $O(\log_2(n))$.

2.2. Algorithme d'Euclide

On s'intéresse au calcul du plus grand diviseur commun (PGCD) de deux entiers naturels $a$ et $b$ qui ne sont pas tous les deux nuls par l'algorithme d'Euclide, en effectuant des divisions euclidiennes successives :

  • On effectue la division euclidienne de $a$ par $b$, qui s'écrit $a = bq + r$, avec $r\in\db{0,b-1}$.
  • On recommence avec les entiers $b$ et $r$ : on écrit la division euclidienne de $b$ par $r$.
  • On s'arrête quand une des divisions euclidiennes donne $0$. Le pgcd de $a$ et $b$ est alors le dernier reste non nul trouvé.

La correction de l'algorithme repose sur les propriétés suivantes.

  1. Si $b=0$, $\op{pgcd}(a,b) = a$
  2. Si $a\geq b$, $b\neq 0$, et $a = bq + r$, alors $\op{pgcd}(a,b) = \op{pgcd}(b,r)$.

Écrire une fonction pgcd_rec récursive qui calcule le pgcd de deux entiers naturels en argument.

Algorithme d'Euclide étendu

Dans le calcul du pgcd de $a,b$ par l'algorithme d'Euclide, notons $a_n, b_n$ les entiers considérés à l'issue de la $n$-ième itération de la boucle, définis par $$\begin{cases}a_0 = a \\ b_0 = b \end{cases}\quad \et \quad \forall n\in\N,\,\begin{cases}a_{n+1} = b_n \\ b_{n+1} = r_n \end{cases}, \text{ où } a_n = b_n q_n + r_n \text{ est la division euclidienne.}$$

On se convainc aisément que pour tout $n\in\N$, $a_n$ et $b_n$ sont des combinaisons linéaires entières des entiers $a,b$ d'origine, c'est-à-dire qu'il existe $u_n,v_n\in\Z$ tels que $b_n = u_n a + v_n b$. En particulier, à la dernière étape, $\op{pgcd}(a,b)$ est lui-même une combinaison linéaire de $a,b$ :

Soient $a,b\in\N$. Il existe un couple $(u,v)\in\Z^2$ tel que $$\op{pgcd}(a,b) = au + bv.$$

L'ensemble $\{au + bv,\, u,v\in\Z\}$ est l'ensemble des multiples de $\op{pgcd}(a,b)$.

Écrire une fonction récursive eucl_etendu qui prend en argument deux entiers $a,b\in\N$ et renvoie un couple de Bézout de $a,b$, c'est-à-dire un couple d'entiers relatifs $(u,v)$ tel que $au + bv = \op{pgcd}(a,b)$.

2.3. Recherche dichotomique récursive

  1. Expliquer pourquoi l'implémentation mauvaise_rech_dicho n'est pas efficace.
  2. Compléter l'implémentation rech_dicho, qui utilise une fonction auxiliaire récursive.
def mauvaise_rech_dicho(t, e):
    n = len(t)
    if n == 0: return False
    if n == 1: return t[0] == e
    m, c = n // 2, t[n // 2]
    if e > c:
        return mauvaise_rech_dicho(t[m+1:])
    if e < c:
        return mauvaise_rech_dicho(t[:m])
    return True
def rech_dicho(t, e):
    # Fonction auxiliaire récursive, qui renvoie True ssi
    # e est présent, entre les indices a inclus et b exclu.
    def rech_aux(a, b):
             # On peut faire référence à t et e ici.

    # La fonction rech_dicho se contente d'appeler rech_aux
    return rech_aux(0, len(t))

2.4. Tri fusion

Pour trier une liste, l'algorithme de tri fusion consiste en les étapes suivantes.

  1. On découpe la liste en deux listes de tailles égales (à peu près).
  2. Par des appels récursifs, on trie chacune des deux listes.
  3. On fusionne les deux listes triées, en une seule liste triée.

  1. Écrire une fonction decoupe qui prend en argument une liste l et renvoie un couple de listes l1 et l2 telles que len(l1) == len(l2) ou len(l1) == len(l2) - 1.
  2. Écrire une fonction fusion qui prend en argument deux listes l1 et l2 triées et qui renvoie une nouvelle liste triée, contenant tous les éléments de l1 et de l2. 2
  3. Écrire une fonction récursive tri_fusion. Celle-ci renverra une nouvelle liste.

Analyse de la complexité temporelle

On note $F_n$ le nombre d'opérations effectuées, dans le pire des cas, par un appel tri_fusion sur une liste de longueur $n$. On suppose que la suite $(F_n)_{n\in\N}$ est croissante3.

Lors de l'appel tri_fusion(n) on

  1. découpe la liste en deux listes, de tailles $\lfloor \frac{n}{2}\rfloor$ et $\lceil \frac{n}{2} \rceil$
  2. appelle tri_fusion sur les deux listes obtenues
  3. fusionne les deux listes triées

Les étapes 1. et 3. ont une complexité linéaire. L'étape 2. réalise au plus $F_{\lfloor \frac{n}{2}\rfloor} + F_{\lceil \frac{n}{2} \rceil}$ opérations.

On en déduit qu'il existe $C\in\R_+$ tel que $$\forall n\geq 2,\, F_n \leq Cn + F_{\lfloor \frac{n}{2}\rfloor} + F_{\lceil \frac{n}{2} \rceil}.$$

Pour simplifier l'étude, on suppose dans un premier temps que $n = 2^p$. On a alors $\forall p,\,F_{2^p} \leq C 2^p + 2F_{2^{p-1}}$.

En réutilisant successivement cette même inégalité, on obtient

$$\begin{aligned}F_{2^p} & \leq C 2^p + 2F_{2^{p-1}} \leq C2^p + 2(C 2^{p-1} + 2 F_{2^{p-2}}) = C 2^p + C 2^p + 4 F_{p-2} \\ & \leq C 2^p + C 2^p + C 2^p + 2^3 F_{p-3} \\ & \leq \, \dots \\ & \leq p C 2^p + 2^p F_1 = C p 2^p\end{aligned}$$

Pour $n$ quelconque, on pose $p = \lfloor \log_2(n)\rfloor + 1$, de sorte que $n\lt 2^p$ et $2^p \leq 2n$. On a $$F_n \leq F_{2^p} \leq C p 2^p \leq 2C(\log_2 (n) + 1)n \leq C' n \ln n.$$

L'algorithme de tri fusion a une complexité en $O(n\ln n)$ dans le pire des cas, où $n$ est la longueur de la liste.

Optimalité de la complexité ★

On s'intéresse à des algorithmes de tri dont la logique n'utilise que des comparaisons entre les éléments permettant de trier toutes les permutations de la liste l = [1, 2, …, n]. Pour toute permutation $\sigma$, on note $N_{\sigma}$ le nombre de comparaisons utilisées par l'algorithme pour trier cette permutation. On note $N = \max_{\sigma}(N_{\sigma})$ le nombre de comparaisons dans le pire des cas.

On a $N \geq \log_2 (n!)$.

Il y a $n!$ permutations de l, et les réalisations de l'algorithme sur chaque permutation doivent être différentes. Il y a au plus $2^N$ réalisations de l'algorithme, donc $2^N\leq n!$, c'est-à-dire $N\leq \log_2 (n!)$.

$\log_2(n!) = \frac{\ln (n!)}{\ln 2} \sim \frac{n \ln n}{\ln 2}$.

On a $\frac{1}{n!}\sum_{\sigma} N_{\sigma}\geq \log_2 (n!) - 3$.

Pour chaque $\sigma\in\mc S_n$, on note $N_{\sigma}$ le nombre de comparaisons de l'algorithme pour la permutation $\sigma$.

On doit avoir, pour tout $k$, $\big|\{\sigma \mid N_{\sigma} \leq k\}\big|\leq 2^k$, donc $\big|\{\sigma \mid N_{\sigma} \geq k\}\big|\geq n! - 2^{k-1}$. Alors $$\sum_{\sigma} N_{\sigma} = \sum_{k = 1}^{\i}\sum_{\sigma \mid N_{\sigma} = k} k = \sum_{k = 1}^{+\i} \sum_{\sigma \mid N_{\sigma}\geq k} 1 \geq \sum_{k =1 }^{2^{k-1} \leq n!} (n! - 2^{k-1}) = \big(\lfloor \log_2(n!)\rfloor + 1\big) n! - \big(2^{\lfloor \log_2(n!)\rfloor + 1} - 1\big)\geq (\log_2(n!) - 3)n!$$

Complexité spatiale ★

Malgré sa complexité temporelle en $O(n\ln n)$, la version implémentée dans l'exercice 2.4 est lente, à cause du grand nombre de copies de listes réalisées (chaque appel l[i:j] réalise une copie).

Bien que ces copies ne changent pas l'ordre de grandeur du nombre d'opérations total, elles changent l'ordre de grandeur de la quantité de mémoire utilisée. On pourrait justifier que la quantité de mémoire totale utilisée (on parle de complexité spatiale) est de l'ordre de $O(n\ln n)$, alors qu'un algorithme optimal n'en utilise que $O(n)$.

Ce défaut n'est pas intrinsèque au tri fusion, uniquement à cette implémentation en Python. Voici une autre implémentation du tri fusion qui n'alloue que $O(n)$ cases mémoires supplémentaires. Autre différence : au lieu de renvoyer une nouvelle liste triée, elle modifie la liste donnée en argument.

def fusion(l):
    # liste tampon
    t = [0] * len(l)
    # On définit une fonction auxiliaire, qui prend
    # en argument deux entiers a, b et qui va trier
    # la liste, entre les indices a (inclus) et b
    # (exclu)
    def fusion_aux(a, b):
        # Pour un seul élément, il n'y a rien à
        # faire
        if b - a <= 1:
            return
        c = (a + b) // 2
        # On trie les deux moitiés
        fusion_aux(a, c)
        fusion_aux(c, b)

        # On fusionne les deux moitiés dans le
        # tampon (de a à b)
        i, j = 0, 0
        while i < (c-a) and j < (b-c):
            if l[a+i] <= l[c+j]:
                t[a+i+j] = l[a+i]
                i += 1
            else:
                t[a+i+j] = l[c+j]
                j += 1
        if i == c-a:
            for k in range(j, b-c):
                t[a+i+k] = l[c+k]
        else:
            for k in range(i, c-a):
                t[a+k+j] = l[a+k]
        # On recopie la liste t dans l
        for k in range(a,b):
            l[k] = t[k]
    # Appel de la fonction auxiliaire
    fusion_aux(0, len(l))

2.5. Tri pivot

Pour trier une liste, l'algorithme de tri par pivot consiste en les étapes suivantes.

  1. On choisit un élément de la liste, (en pratique, son premier élément), qui va jouer le rôle de pivot.
  2. On modifie la liste de sorte que tous les éléments inférieurs ou égaux au pivot soient à sa gauche, et les autres éléments soient à sa droite. En particulier, le pivot est à sa place finale, une fois la liste triée.
  3. Par des appels récursifs, on trie chacune des deux sous-listes, à droite et à gauche du pivot.

Cet algorithme modifiera la liste sur place plutôt que d'en créer une nouvelle.

Implémenter ce tri. On utilisera une fonction auxiliaire tri_pivot_aux qui prend en argument deux entiers a,b et qui se charge de trier la sous-liste de la liste d'origine l entre les indices a (inclus) et b (exclu).

def tri_pivot(l):
    # Définition d'une fonction auxiliaire, à l'intérieur de
    # tri_pivot. L'appel tri_pivot_aux(a, b) doit trier la liste
    # l entre les indices a et b
    def tri_pivot_aux(a, b):
        ...
    # Appel de la fonction auxilaire, sur toute la liste
    tri_pivot_aux(0, len(l))

Complexité du tri pivot

L'algorithme est d'autant plus rapide que les pivots successifs partitionnent la liste en deux parties de tailles égales. Dans le meilleur des cas, si le pivot est une médiane à chaque étape, la complexité vérifie la même relation de récurrence que le tri fusion : $F_n = F_{\lfloor \frac{n}{2}\rfloor} + F_{\lceil \frac{n}{2} \rceil} + Cn$, qui donnerait une complexité en $O(n\ln n)$.

Dans le pire des cas, si tous les pivots choisis sont des extremums de la liste, on obtient une relation de récurrence en $F_n = Cn + F_{n-1}$, qui donne $F_n = Cn + C (n-1) + \dots + C = C \frac{n(n+1)}{2}$, donc une complexité quadratique.

C'est le cas si la liste en entrée est déjà triée, par ordre croissant ou décroissant.

«En moyenne», le tri pivot a une complexité en $O(n\ln n)$.

Malgré sa complexité quadratique dans le pire des cas, le tri pivot est en pratique plus rapide que le tri fusion (entre autres parce qu'il n'alloue pas de mémoire supplémentaire). Il est parfois appelé tri rapide, et quicksort en anglais.

Il est possible (mais très technique) de modifier l'algorithme pour choisir le pivot de manière plus intelligente, de sorte à garantir une complexité en $O(n\ln n)$ dans le pire des cas.

2.6. Parcours en profondeur récursif d'un arbre ★

On considère un arbre, dont les sommets sont les entiers de $0$ à $n-1$, et dont la racine est le sommet $0$.

On représente cet arbre par une liste enfants de taille $n$, où enfants[i] est la liste des enfants du sommet $i$.

À partir de la racine, on veut parcourir cet arbre en profondeur, et appeler une fonction f donnée à chaque sommet de l'arbre, dans l'ordre du parcours.

Un parcours en profondeur (de gauche à droite) est un parcours dans lequel si un sommet à plusieurs enfants $e_1,\dots, e_m$, on visite d'abord tous les descendants de $e_1$, puis tous les descendants de $e_2$, etc.

Parmi les parcours en profondeurs, on distingue

  • le parcours préfixe, où l'on appelle la fonction f sur un sommet $s$ avant de l'appeler sur ses descendants.
  • le parcours infixe, où l'on appelle la fonction f après l'avoir appelé sur ses descendants.

Implémenter deux fonctions parcours_prefixe et parcours_infixe qui prennent en argument la liste enfants et une fonction f et réalisent les parcours décrits.

3. Exercices

Écrire une fonction récursive decomp qui prend en argument un entier $n\in\N^*$ et renvoie l'unique paire d'entiers $(p, m)$ avec $p\in\N$ et $m\in\N^*$ impair telle que $n = 2^p m$.

Par exemple, l'entier $12$ s'écrit comme $12 = 4\times 3 = 2^2 \times 3$.

Essayez : decomp

  1. Écrire une fonction récursive somme_chiffres qui calcule la somme des chiffres décimaux d'un entier naturel.
  2. Écrire une fonction récursive chiffres_pairs qui prend en argument un entier naturel n et renvoie True si tous ses chiffres décimaux sont pairs.
Essayez : somme_chiffreschiffres_pairs

On représente une partie de $\db{1,n}$ par la liste de ses éléments, par ordre croissant. On veut écrire une fonction parties qui prend $n$ en argument et renvoie la liste des parties de $\db{1,n}$.

  1. Par convention, parties(0) renvoie la liste [[]], puisque l'ensemble vide $\emptyset$ admet une unique partie.

    Que renvoient parties(1) et parties(2) ?

  2. Implémenter la fonction parties, d'une manière récursive.

    On remarquera qu'il y a deux types de parties de $\db{1,n}$ : celles qui ne contiennent pas $n$, et qui sont donc des parties de $\db{1,n-1}$ et celles qui contiennent $n$.

Essayez : Question 1parties

Alice et Bob jouent au jeu suivant : $n$ bâtonnets sont alignés. Tour à tour, les joueurs doivent retirer un, deux ou trois bâtonnets. Le joueur qui ne peut pas retirer de bâtonnet (s'il n'y en a plus) perd.

Si $n=1,2,3$, le premier joueur a une stratégie gagnante (laquelle ?). Quel que soit $n$, un des deux joueurs a une stratégie gagnante.

Écrire une fonction est_gagnant qui prend en argument un entier n et qui renvoie True si le premier joueur a une stratégie gagnante, et False sinon.

On peut décrire simplement la stratégie gagnante, mais ce n'est pas l'objectif ici.

Essayez : est_gagnant

Écrire une fonction chaine_extraite qui prend en argument deux chaînes c1 et c2 et qui renvoie True si la chaîne c2 peut être obtenue à partir de la chaîne c1 en retirant certains caractères.

  • assert(chaine_extraite("aabbcc", "abc"))
  • assert(not(chaine_extraite("ababc", "baba")))

Une permutation de $\db{1,n}$ est une liste de taille $n$ contenant tous les entiers de $1$ à $n$.

  1. Écrire une fonction permutations qui renvoie la liste de toutes les permutations de $\db{1,n}$.
  2. Combien y a-t-il de permutations de $\db{1,n}$ ?

Dans le jeu des tours de Hanoï, $n$ disques de largeurs $1,2,\dots, n$ sont initialement empilés sur une tour, du plus large au moins large. Deux autres tours, vides, sont disponibles. À chaque étape, on peut déplacer un disque, situé en haut d'une des tours sur une autre tour, mais un disque ne peut être posé que sur un disque plus large. Le but du jeu est de déplacer toute la tour initiale sur la troisième tour.

Hanoi.jpeg

  1. Décrire une stratégie récursive.
  2. Écrire une fonction tour_hanoi qui prend en argument un entier $n$ et qui résout le problème, en imprimant une liste d'instructions de la forme

    «On déplace le disque de largeur $\dots$ de la tour $\dots$ à la tour $\dots$».

Notes de bas de page:

1

Vérifier que la suite $V_n = T_n + 2$ satisfait une équation de récurrence linéaire d'ordre $2$.

2

On ajoutera petit à petit des éléments dans la nouvelle liste, en progressant alternativement dans l1 ou l2. On utilisera deux variables intermédiaires i1 et i2 représentant les indices où on en est dans l1 et l2.

3

Si elle ne l'était pas, il faudrait remplacer les $F_n$, par $G_n = \max\limits_{1\leq k\leq n} F_k$. La suite $(G_n)$ est forcément croissante et vérifie la même inéquation de récurrence.